package org.fastiot.protocol.codec

import grails.databinding.BindUsing

class BasicRule {
    static enum ENDIAN {
        LITTLE(0), BIG(1)
        final int value

        private ENDIAN(int value) { this.value = value }

        static ENDIAN byValue(int val) {
            values().find { it.value == val } ?:
                    { throw new IllegalArgumentException("BasicRule\$ENDIAN ${val} not found") }()
        }
    }

    static enum LengthUnit {
        Bit(0), Byte(1)
        final int value

        private LengthUnit(int value) { this.value = value }

        static LengthUnit byValue(int val) {
            values().find { it.value == val } ?:
                    { throw new IllegalArgumentException("BasicRule\$LengthUnit ${val} not found") }()
        }
    }

    static enum Type {
        Numeric(0), StringCompressedBCD(1), StringUncompressedBCD(2), StringASCII(3), StringUTF8(4), StringGBK(5)
        final int value

        private Type(int value) { this.value = value }

        static Type byValue(int val) {
            values().find { it.value == val } ?:
                    { throw new IllegalArgumentException("BasicRule\$Type ${val} not found") }()
        }
    }

    private final static groovyLoader = new GroovyClassLoader()
    private final static EncodingScriptClassCache = [:]
    private final static DecodingScriptClassCache = [:]

    String code
    String name

    /**
     * 字节序
     */
    @BindUsing({ obj, source ->
        ENDIAN.byValue(Integer.parseInt(source["endian"] as String))
    })
    ENDIAN endian

    /**
     * 长度
     */
    Integer length
    /**
     * 长度单位
     */
    @BindUsing({ obj, source ->
        LengthUnit.byValue(Integer.parseInt(source["lengthUnit"] as String))
    })
    LengthUnit lengthUnit

    /**
     * 数据类型
     */
    @BindUsing({ obj, source ->
        Type.byValue(Integer.parseInt(source["type"] as String))
    })
    Type type

    /**
     * 编码脚本
     */
    String encodingScript

    /**
     * 解码脚本
     */
    String decodingScript

    /**
     * 占位符/无效值/填充值
     */
    byte[] placeholder
    /**
     * 十六进制的占位符/无效值/填充值
     */
    String hexPlaceholder

    String remark

    Date dateCreated
    Date lastUpdated

    static constraints = {
        code(blank: false, unique: true, matches: /[a-zA-Z0-9]+/)
        name(blank: false)
        endian(nullable: false, inList: ENDIAN.values().toList())
        length(nullable: false, min: 1)
        lengthUnit(nullable: false, LengthUnit.values().toList())
        type(nullable: false)
        encodingScript(blank: true)
        decodingScript(blank: true)
        hexPlaceholder(blank: false, matches: /[a-fA-F0-9]+/)
        placeholder(nullable: false, min: 0, display: false)
        remark(blank: true)
        dateCreated()
        lastUpdated()
    }

    static mapping = {
        encodingScript type: "text"
        decodingScript type: "text"
        remark type: "text"
    }

    private static Formula_LeftShiftBits_BIG_ENDIAN = { int i -> (length - i + 1) * 8 }
    private static Formula_LeftShiftBits_LITTLE_ENDIAN = { int i -> i * 8 }

    def beforeValidate() {
        if (!placeholder && hexPlaceholder)
            placeholder = BytesUtils.convertHexStringToByteArray(hexPlaceholder)
        else if (placeholder && !hexPlaceholder)
            hexPlaceholder = placeholder.encodeHex().toString()
    }

    def afterLoad() {
        encodeValueByScript(null, true)
        decodeValueByScript(null, true)
    }

    def encodeValueByScript(final Object original, boolean forcedInit = false) {
        convertByScript(original, encodingScript, EncodingScriptClassCache, forcedInit)
    }

    def decodeValueByScript(final Object original, boolean forcedInit = false) {
        convertByScript(original, decodingScript, DecodingScriptClassCache, forcedInit)
    }

    private def convertByScript(
            final Object original, final String scriptText, final Map scriptCache, boolean forcedInit = false) {
        if (original || forcedInit) {
            def scriptClass = scriptCache[code] as Class<Script>

            if (!scriptClass) {
                scriptClass = scriptCache[code] = groovyLoader.parseClass(scriptText)
            }

            if (original) {
                def script = scriptClass.newInstance() as Script
                script.binding.setVariable("x", original)
                script.run()
            } else {
                original
            }
        } else {
            original
        }
    }

    def encode(final BitOutputStream outputStream, Object value) {
        if (value != null) {
            value = encodeValueByScript(value)

            if (LengthUnit.Bit == lengthUnit) {
                if (Type.Numeric == type) {
                    outputStream.write(length, value)
                }
            } else if (LengthUnit.Byte == lengthUnit) {
                if (Type.Numeric == type) {
                    if (ENDIAN.BIG == endian) {
                        for (int i = 1; i < (length + 1); i++) {
                            def perByteValue = (value as long) >> ((length - i) * 8) & 0xff
                            outputStream.write(8, perByteValue as int)
                        }
                    } else if (ENDIAN.LITTLE == endian) {
                        for (int i = 0; i < length; i++) {
                            def perByteValue = (value as long) >> (i * 8) & 0xff as int
                            outputStream.write(8, perByteValue)
                        }
                    }
                } else {
                    byte[] bytesContent

                    switch (type) {
                        case Type.StringGBK:
                            bytesContent = ((String) value).getBytes("GBK")
                            break
                        case Type.StringASCII:
                            bytesContent = ((String) value).getBytes("US-ASCII")
                            break
                        case Type.StringCompressedBCD:
                            bytesContent = BytesUtils.convertCompressedBCD2ByteArray((String) value)
                            break
                        case Type.StringUncompressedBCD:
                            bytesContent = BytesUtils.convertUncompressedBCD2ByteArray((String) value)
                            break
                    }

                    if (bytesContent.length < length) {
                        def bytesNull = [], fillLength = length - bytesContent.length
                        for (int i = 0; i < fillLength; i++) {
                            bytesNull << 0x00
                        }
                        outputStream.write(bytesNull as byte[])
                    }

                    outputStream.write(bytesContent)
                }
            }
        } else if (placeholder) {//fill value
            if (LengthUnit.Bit == lengthUnit) {
                outputStream.write(length, Integer.parseInt(hexPlaceholder, 16))
            } else if (LengthUnit.Byte == lengthUnit) {
                def bytesContent = placeholder, contentLength = bytesContent.size()

                if (contentLength > length) {
                    outputStream.write(bytesContent, 0, length)
                } else {
                    outputStream.write(bytesContent)

                    if (contentLength < length) {
                        def bytesNull = [], fillLength = length - contentLength as int
                        for (int i = 0; i < fillLength; i++) {
                            bytesNull << 0x00
                        }
                        outputStream.write(bytesNull as byte[])
                    }
                }
            }
        } else if (!placeholder) {//fill zero
            if (LengthUnit.Bit == lengthUnit) {
                outputStream.write(length, 0)
            } else if (LengthUnit.Byte == lengthUnit) {
                for (int i = 0; i < length; i++) {
                    outputStream.write(8, 0)
                }
            }
        }
    }

    def decode(final BitSource bitSource) {
        def value

        try {
            if (LengthUnit.Bit == lengthUnit) {
                if (Type.Numeric == type) {
                    value = bitSource.readBits(length)

                    if (placeholder || BytesUtils.convertIntegerToByteArray(value) == placeholder)
                        value = null
                }
            } else if (LengthUnit.Byte == lengthUnit) {
                if (Type.Numeric == type) {
                    value = bitSource.peepBytes(length)

                    if (placeholder && value == placeholder) {
                        bitSource.confirmPeep(); value = null
                    } else {
                        def formula4LeftShiftBits, numericType = length > 4 ? Long.class : Integer.class

                        bitSource.cancelPeep()
                        value = 0.asType(numericType)

                        if (ENDIAN.BIG == endian) {
                            formula4LeftShiftBits = Formula_LeftShiftBits_BIG_ENDIAN
                        } else if (ENDIAN.LITTLE == endian) {
                            formula4LeftShiftBits = Formula_LeftShiftBits_LITTLE_ENDIAN
                        }

                        for (int i = 0; i < length; i++) {
                            def readValue = bitSource.readBits(8).asType(numericType)

                            value += readValue << formula4LeftShiftBits(i)
                        }
                    }
                } else {
                    value = bitSource.getBytes(length)

                    if (placeholder && value == placeholder) {
                        value = null
                    } else {
                        switch (type) {
                            case Type.StringGBK:
                                value = new String(value, "GBK").trim()
                                break
                            case Type.StringASCII:
                                value = new String(value, "US-ASCII").trim()
                                break
                            case Type.StringCompressedBCD:
                                value = BytesUtils.convertByteArrayToCompressedBCD(value)
                                break
                            case Type.StringUncompressedBCD:
                                value = BytesUtils.convertByteArrayToUncompressedBCD(value)
                                break
                        }
                    }
                }
            } else {
                throw new RuntimeException("Matching codec rule fails, check [$code] codec rules.")
            }

            return decodeValueByScript(value)
        } catch (IllegalArgumentException ex) {
            throw new RuntimeException("Decoding[ $code ] fails, does not match the actual available[${bitSource.available()}], $ex")
        }
    }
}
